package imageprocessing;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.util.Arrays;
import java.util.Collections;

public class DePaper 
{
	private static BufferedImage blurImage(BufferedImage img, int radius)
	{
		int width = img.getWidth();
		int height = img.getHeight();
		
		BufferedImage tmp = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
		BufferedImage dst = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
		
		if(radius == 0)
			radius = Math.min(img.getWidth(), img.getHeight()) / 15;
		
		byte[] pixelsA = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
		byte[] pixelsB = ((DataBufferByte) tmp.getRaster().getDataBuffer()).getData();
		byte[] pixelsC = ((DataBufferByte) dst.getRaster().getDataBuffer()).getData();
		
		float[] gaussPrecalc = new float[radius];
		float gaussSum = 0.0f;
		
		for(int i = 0; i < radius; ++i)
			gaussSum += gaussPrecalc[i] = (float)Math.cos(((double)i / (double)radius) * Math.PI) * 0.5f + 0.5f;
		
		for(int i = 0; i < radius; ++i)
			gaussPrecalc[i] = (gaussPrecalc[i] / gaussSum) * 0.5f;
		
		float[] pixSum = new float[3];
		
		for(int y = 0; y < height; ++y)
		{
			int startY = y * width * 3;
			
			for(int x = 0; x < width; ++x)
			{
				pixSum[0] = 0.0f; pixSum[1] = 0.0f; pixSum[2] = 0.0f;
				
				for(int offset = 0; offset < radius; ++offset)
				{
					int clampedPosL = x - offset;
					int clampedPosR = x + offset;
					if(clampedPosL < 0) clampedPosL = 0;
					if(clampedPosR >= width) clampedPosR = width - 1;
					
					
					pixSum[0] += (pixelsA[startY + clampedPosL * 3 + 0] & 0xFF) * gaussPrecalc[offset];
					pixSum[1] += (pixelsA[startY + clampedPosL * 3 + 1] & 0xFF) * gaussPrecalc[offset];
					pixSum[2] += (pixelsA[startY + clampedPosL * 3 + 2] & 0xFF) * gaussPrecalc[offset];
					
					pixSum[0] += (pixelsA[startY + clampedPosR * 3 + 0] & 0xFF) * gaussPrecalc[offset];
					pixSum[1] += (pixelsA[startY + clampedPosR * 3 + 1] & 0xFF) * gaussPrecalc[offset];
					pixSum[2] += (pixelsA[startY + clampedPosR * 3 + 2] & 0xFF) * gaussPrecalc[offset];
				}
				
				pixelsB[startY + x * 3 + 0] = (byte)(pixSum[0]);
				pixelsB[startY + x * 3 + 1] = (byte)(pixSum[1]);
				pixelsB[startY + x * 3 + 2] = (byte)(pixSum[2]);
			}
		}
		
		int tripleW = width * 3;
		
		for(int y = 0; y < height; ++y)
		{	
			for(int x = 0; x < width; ++x)
			{
				int start = y * width * 3 + x * 3;
				pixSum[0] = 0.0f; pixSum[1] = 0.0f; pixSum[2] = 0.0f;
				
				for(int offset = 0; offset < radius; ++offset)
				{
					int clampedPosU = y - offset;
					int clampedPosD = y + offset;
					if(clampedPosU < 0) clampedPosU = 0;
					if(clampedPosD >= height) clampedPosD = height - 1;
					
					
					pixSum[0] += (pixelsB[clampedPosU * tripleW + x * 3 + 0] & 0xFF) * gaussPrecalc[offset];
					pixSum[1] += (pixelsB[clampedPosU * tripleW + x * 3 + 1] & 0xFF) * gaussPrecalc[offset];
					pixSum[2] += (pixelsB[clampedPosU * tripleW + x * 3 + 2] & 0xFF) * gaussPrecalc[offset];
					
					pixSum[0] += (pixelsB[clampedPosD * tripleW + x * 3 + 0] & 0xFF) * gaussPrecalc[offset];
					pixSum[1] += (pixelsB[clampedPosD * tripleW + x * 3 + 1] & 0xFF) * gaussPrecalc[offset];
					pixSum[2] += (pixelsB[clampedPosD * tripleW + x * 3 + 2] & 0xFF) * gaussPrecalc[offset];
				}
				
				pixelsC[start + 0] = (byte)(pixSum[0]);
				pixelsC[start + 1] = (byte)(pixSum[1]);
				pixelsC[start + 2] = (byte)(pixSum[2]);
			}
		}
		
		return dst;
	}
	
	private static int generateDelta(BufferedImage imgA, BufferedImage imgB, byte[] delta, int threshold)
	{
		int count = 0;
		
		byte[] pixelsA = ((DataBufferByte) imgA.getRaster().getDataBuffer()).getData();
		byte[] pixelsB = ((DataBufferByte) imgB.getRaster().getDataBuffer()).getData();
		
		int pixCount = imgA.getWidth() * imgB.getHeight();
		int byteCount = pixCount * 3;
		int dR, dG, dB;
		
		//int threshold = 96; //128;
		
		for(int i = 0; i < byteCount; i += 3)
		{
			dB = Math.abs((pixelsA[i+0] & 0xFF) - (pixelsB[i+0] & 0xFF));
			dG = Math.abs((pixelsA[i+1] & 0xFF) - (pixelsB[i+1] & 0xFF));
			dR = Math.abs((pixelsA[i+2] & 0xFF) - (pixelsB[i+2] & 0xFF));
			
			if(dR + dG + dB > threshold)
				delta[i / 3] = 1;
			else
				delta[i / 3] = 0;
		}
		
		return count;
	}
	
	private static void dilate(byte[] src, byte[] dst, int width, int height)
	{
		for(int y = 1; y < height - 1; ++y)
		{
			for(int x = 1; x < width - 1; ++x)
			{
				if((src[(y+0)*width+(x+0)] > 0) ||
						(src[(y-1)*width+(x-1)] > 0) ||
						(src[(y-1)*width+(x+0)] > 0) ||
						(src[(y-1)*width+(x+1)] > 0) ||
						(src[(y+0)*width+(x-1)] > 0) ||
						(src[(y+0)*width+(x+1)] > 0) ||
						(src[(y+1)*width+(x-1)] > 0) ||
						(src[(y+1)*width+(x+0)] > 0) ||
						(src[(y+1)*width+(x+1)] > 0))
				{
					dst[y*width+x] = 1;
				}
				else
				{
					dst[y*width+x] = 0;
				}
			}
		}
	}
	
	
	
	private static int sampleMaskedBox(int X, int Y, byte[] pixels, byte[] mask, int width, int height, int[] samples)
	{
		int count = 0;
		samples[0] = 0; samples[1] = 0; samples[2] = 0;
		
		for(int y = Y - 2; y <= Y + 2; ++y)
		{
			if(y < 0) continue;
			if(y >= height) continue;
			
			for(int x = X - 2; x <= X + 2; ++x)
			{
				if(x < 0) continue;
				if(x >= width) continue;
				
				if(mask[y * width + x] == 0)
				{
					samples[0] += pixels[(y * width + x) * 3 + 0] & 0xFF;
					samples[1] += pixels[(y * width + x) * 3 + 1] & 0xFF;
					samples[2] += pixels[(y * width + x) * 3 + 2] & 0xFF;
					count++;
				}
			}
		}
		
		if(count > 0)
		{
			samples[0] /= count;
			samples[1] /= count;
			samples[2] /= count;
		}
		
		return count;
	}
	
	private static void interpolateMarked(BufferedImage src, BufferedImage dst, byte[] mask)
	{
		int width = src.getWidth();
		int height = src.getHeight();
		
		byte[] pixelsA = ((DataBufferByte) src.getRaster().getDataBuffer()).getData();
		byte[] pixelsB = ((DataBufferByte) dst.getRaster().getDataBuffer()).getData();
		//byte[] pixelsR = ((DataBufferByte) rep.getRaster().getDataBuffer()).getData();
		
		int[] history = new int[width * height];
		
		int[] samplesA = new int[3];
		int[] samplesB = new int[3];
		
		for(int y = 0; y < height; ++y)
		{
			for(int x = 0; x < width; ++x)
			{
				if(mask[y*width+x] > 0 || x == 0)
				{
					int runLength = 0;
					if(0 == sampleMaskedBox(x - 1, y, pixelsA, mask, width, height, samplesA))
					{
						samplesA[0] = pixelsA[(y*width + x) * 3 + 0] & 0xFF;
						samplesA[1] = pixelsA[(y*width + x) * 3 + 1] & 0xFF;
						samplesA[2] = pixelsA[(y*width + x) * 3 + 2] & 0xFF;
					}
					
					int foundCount = 0;
					for(int X = x + 1; X < width; ++X)
					{
						if(mask[y*width+X] == 0)
						{
							foundCount = sampleMaskedBox(X, y, pixelsA, mask, width, height, samplesB);
							runLength = X - x;
							break;
						}
					}
					
					if(foundCount == 0)
					{
						runLength = width - x - 1;
						samplesB[0] = pixelsA[(y*width + width - 1) * 3 + 0] & 0xFF;
						samplesB[1] = pixelsA[(y*width + width - 1) * 3 + 1] & 0xFF;
						samplesB[2] = pixelsA[(y*width + width - 1) * 3 + 2] & 0xFF;
					}
					
					samplesB[0] -= samplesA[0];
					samplesB[1] -= samplesA[1];
					samplesB[2] -= samplesA[2];
					
					for(int X = x; X <= x + runLength; ++X)
					{
						float factor = (float)(X - x) / (float)(runLength);
						pixelsB[(y*width + X) * 3 + 0] = (byte)(samplesA[0] + (int)(samplesB[0] * factor));  
						pixelsB[(y*width + X) * 3 + 1] = (byte)(samplesA[1] + (int)(samplesB[1] * factor));
						pixelsB[(y*width + X) * 3 + 2] = (byte)(samplesA[2] + (int)(samplesB[2] * factor));
						
						history[y*width+X] = runLength;
					}
					
					x += runLength;
				}
				else
				{
					pixelsB[(y*width + x) * 3 + 0] = pixelsA[(y*width + x) * 3 + 0];  
					pixelsB[(y*width + x) * 3 + 1] = pixelsA[(y*width + x) * 3 + 1];
					pixelsB[(y*width + x) * 3 + 2] = pixelsA[(y*width + x) * 3 + 2];
				}
			}
		}
		
		for(int x = 0; x < width; ++x)
		{
			for(int y = 0; y < height; ++y)
			{
				if(mask[y*width+x] > 0 || y == 0)
				{
					int runLength = 0;
					int foundCount = sampleMaskedBox(x, y - 1, pixelsA, mask, width, height, samplesA);
					if(foundCount == 0)
					{
						samplesA[0] = pixelsA[(y*width + x) * 3 + 0] & 0xFF;
						samplesA[1] = pixelsA[(y*width + x) * 3 + 1] & 0xFF;
						samplesA[2] = pixelsA[(y*width + x) * 3 + 2] & 0xFF;
					}
					
					foundCount = 0;
					for(int Y = y + 1; Y < height; ++Y)
					{
						if(mask[Y*width+x] == 0)
						{
							foundCount = sampleMaskedBox(x, Y, pixelsA, mask, width, height, samplesB);
							runLength = Y - y;
							break;
						}
					}
					
					if(foundCount == 0)
					{
						runLength = height - y - 1;
						samplesB[0] = pixelsA[((height-1)*width + x) * 3 + 0] & 0xFF;
						samplesB[1] = pixelsA[((height-1)*width + x) * 3 + 1] & 0xFF;
						samplesB[2] = pixelsA[((height-1)*width + x) * 3 + 2] & 0xFF;
					}
					
					samplesB[0] -= samplesA[0];
					samplesB[1] -= samplesA[1];
					samplesB[2] -= samplesA[2];
					
					for(int Y = y; Y <= y + runLength; ++Y)
					{
						float factor = (float)(Y - y) / (float)(runLength);
						float blend = (float)history[Y*width+x] / (float)(history[Y*width+x] + runLength);
						
						pixelsB[(Y*width + x) * 3 + 0] = (byte)
								((pixelsB[(Y*width + x) * 3 + 0] & 0xFF) * blend +
										(samplesA[0] + (int)(samplesB[0] * factor)) * (1.0f - blend));
						
						pixelsB[(Y*width + x) * 3 + 1] = (byte)
								((pixelsB[(Y*width + x) * 3 + 1] & 0xFF) * blend +
										(samplesA[1] + (int)(samplesB[1] * factor)) * (1.0f - blend));
						
						pixelsB[(Y*width + x) * 3 + 2] = (byte)
								((pixelsB[(Y*width + x) * 3 + 2] & 0xFF) * blend +
										(samplesA[2] + (int)(samplesB[2] * factor)) * (1.0f - blend));
					}
					
					y += runLength;
				}
			}
		}
	}
	
	private static void generateFinal(BufferedImage imgA, BufferedImage imgB, BufferedImage result)
	{
		byte[] pixelsA = ((DataBufferByte) imgA.getRaster().getDataBuffer()).getData();
		byte[] pixelsB = ((DataBufferByte) imgB.getRaster().getDataBuffer()).getData();
		byte[] pixelsC = ((DataBufferByte) result.getRaster().getDataBuffer()).getData();
		
		int pixCount = imgA.getWidth() * imgA.getHeight(); 
		int byteCount = pixCount * 3;
		int dR, dG, dB;
		
		int[] histogram = new int[26];
		int histAvg = (imgA.getWidth() * imgA.getHeight()) / 25;
		
		if(finalRescale)
		{
			for(int i = 0; i < byteCount; i += 3)
			{
				int sum = (pixelsA[i+0] & 0xFF) + (pixelsA[i+1] & 0xFF) + (pixelsA[i+2] & 0xFF);
				float factor = (3 * 255.0f) / (float)sum;
				
				dB = (int)(factor * ((pixelsB[i+0] & 0xFF) - (pixelsA[i+0] & 0xFF)));
				dG = (int)(factor * ((pixelsB[i+1] & 0xFF) - (pixelsA[i+1] & 0xFF)));
				dR = (int)(factor * ((pixelsB[i+2] & 0xFF) - (pixelsA[i+2] & 0xFF)));
				
				if(dR < 0) dR = 0;
				if(dG < 0) dG = 0;
				if(dB < 0) dB = 0;
				
				if(dR > 255) dR = 255;
				if(dG > 255) dG = 255;
				if(dB > 255) dB = 255;
				
				pixelsC[i+0] = (byte)(255 - dB);
				pixelsC[i+1] = (byte)(255 - dG);
				pixelsC[i+2] = (byte)(255 - dR);
				
				int spot = (((255 - dR) + (255 - dG) + (255 - dB)) / 3) / 10;
				histogram[spot]++;
			}
		}
		else
		{
			for(int i = 0; i < byteCount; i += 3)
			{
				dB = (pixelsB[i+0] & 0xFF) - (pixelsA[i+0] & 0xFF);
				dG = (pixelsB[i+1] & 0xFF) - (pixelsA[i+1] & 0xFF);
				dR = (pixelsB[i+2] & 0xFF) - (pixelsA[i+2] & 0xFF);
				
				if(dR < 0) dR = 0;
				if(dG < 0) dG = 0;
				if(dB < 0) dB = 0;
				
				pixelsC[i+0] = (byte)(255 - dB);
				pixelsC[i+1] = (byte)(255 - dG);
				pixelsC[i+2] = (byte)(255 - dR);
				
				int spot = (((255 - dR) + (255 - dG) + (255 - dB)) / 3) / 10;
				histogram[spot]++;
			}
		}
		
		int histMin = 0, histMax = 25;
		
		for(int i = 0; i < 24; ++i)
		{
			histMin = i;
			if(histogram[i] >= histAvg / 10)
				break;
		}
		
		for(int i = 25; i > 2; --i)
		{
			histMax = i;
			if(histogram[i] >= histAvg / 10)
				break;
		}
		
		float valueScale = 255.0f / (histMax * 10 - histMin * 10);
		
		histMin *= 10;
		
		for(int i = 0; i < byteCount; ++i)
		{
			int p = pixelsC[i] & 0xFF;
			p = (int)((p - histMin) * valueScale);
			if(p < 0) p = 0;
			if(p > 255) p = 255;
			pixelsC[i] = (byte)p;
		}
	}
	
	
	
	public static BufferedImage thresholdBW(BufferedImage image, int thresh, boolean negative)
	{
		BufferedImage result = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY); //Util.sameCanvasSize(image);
		thresh *= 3;
		
		byte[] pixelsA = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
		byte[] pixelsB = ((DataBufferByte) result.getRaster().getDataBuffer()).getData();
		
		int byteCount = image.getWidth() * image.getHeight() * 3;
		
		byte valPos = -1, valNeg = 0;
		if(negative)
		{
			valPos = 0;
			valNeg = -1;
		}
		
		for(int i = 0, n = 0; i < byteCount; i += 3, ++n)
		{
			if((pixelsA[i+0] & 0xFF) + (pixelsA[i+1] & 0xFF) + (pixelsA[i+2] & 0xFF) > thresh)
				pixelsB[n] = valPos;
			else
				pixelsB[n] = valNeg;
		}
		
		return result;
	}
	
	public static int[] generateHistogram(BufferedImage image)
	{
		byte[] pixelsA = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
		int[] histogram = new int[257];
		
		int byteCountA = image.getWidth() * image.getHeight() * 3;
		
		for(int i = 0; i < byteCountA; i += 3)
		{
			int val = Math.round(((pixelsA[i+0] & 0xFF) + (pixelsA[i+1] & 0xFF) + (pixelsA[i+2] & 0xFF)) / 3.0f);
			histogram[val]++;
		}
		
		return histogram;
	}
	
	public static BufferedImage autoThreshold(BufferedImage image)
	{
		BufferedImage bw = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
		byte[] pixelsA = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
		byte[] pixelsB = ((DataBufferByte) bw.getRaster().getDataBuffer()).getData();
		
		int byteCountA = image.getWidth() * image.getHeight() * 3;
		int byteCountB = byteCountA / 3;
		
		int[] histogram = new int[257];
		
		for(int i = 0; i < byteCountA; i += 3)
		{
			int val = Math.round(((pixelsA[i+0] & 0xFF) + (pixelsA[i+1] & 0xFF) + (pixelsA[i+2] & 0xFF)) / 3.0f);
			histogram[val]++;
			pixelsB[i/3] = (byte)val;
		}
		
		int min = 0, max = 255;
		for(int i = 0; i < 255; ++i)
			if(histogram[i] == 0)
				min = i;
			else
				break;
		
		for(int i = 255; i >= 0; --i)
			if(histogram[i] == 0)
				max = i;
			else
				break;
		
		min++;
		max--;
		if(min > 255) min = 255;
		if(max < 0) max = 0;
		
		if(printStats) System.out.println("Histogram min/max = " + min + " -> " + max);
		
		int movingIndex = min;
		int sum1, sum2, sum3, sum4;
		float result = 0.0f;
		
		do {
			sum1 = sum2 = sum3 = sum4 = 0;
			
			for (int i=min; i<=movingIndex; i++) 
			{
				sum1 += i*histogram[i];
				sum2 += histogram[i];
			}
			
			for (int i=(movingIndex+1); i<=max; i++) 
			{
				sum3 += i*histogram[i];
				sum4 += histogram[i];
			}
			
			result = ((float)sum1/(float)sum2 + (float)sum3/(float)sum4)/2.0f;
			movingIndex++;
		} while ((movingIndex+1)<=result && movingIndex<max-1);

		//histogram[0]= count0; histogram[maxValue]=countMax;
		int level = (int)Math.round(result);
		if(printStats) System.out.println("Determined level = " + level);
		
		for(int i = 0; i < byteCountB; ++i)
			pixelsB[i] = ((pixelsB[i] & 0xFF) <= level) ? (byte)0 : (byte)255;
			//pixelsB[i] = ((pixelsB[i] & 0xFF) >= min && (pixelsB[i] & 0xFF) <= level) ? (byte)0 : (byte)255;
		
		return bw;	
	}
	
	private static BufferedImage smallBlur(BufferedImage image, float blurFactor, int resX, int resY)
	{
		if(resX == 0) resX = 256;
		if(resY == 0) resY = 256;
		
		BufferedImage small = new BufferedImage(256, 256, BufferedImage.TYPE_3BYTE_BGR);
		Graphics2D g = (Graphics2D)small.getGraphics();
		g.drawImage(image, 0, 0, 256, 256, null);
		g.dispose();
		BufferedImage smallBlurred = blurImage(small, (int)(256 * blurFactor));
		BufferedImage blurred = Util.sameCanvasSize(image);
		g = (Graphics2D)blurred.getGraphics();
		g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
		g.drawImage(smallBlurred, 0, 0, image.getWidth(), image.getHeight(), null);
		g.dispose();
		return blurred;
	}
	
	public static boolean reduceIntermediateResolution = true;
	public static boolean printStats = false;
	public static boolean finalRescale = false;
	
	public static BufferedImage process(BufferedImage image, float blurFactor, float threshold, boolean removeForeground)
	{
		if(image == null) return null;
		
		int width = image.getWidth();
		int height = image.getHeight();
		
		byte[] delta = new byte[width * height];
		byte[] delta2 = new byte[width * height];
		
		int smallerSide = (width < height) ? width : height;
		
		long miliStart = System.currentTimeMillis();
		long totalStart = miliStart;
		long time = 0;
		
		if(printStats) System.out.println("Blur radius = " + (int)(smallerSide * blurFactor));
		
		if(removeForeground)
		{
			BufferedImage blurred = null;
			
			if(reduceIntermediateResolution)
				blurred = smallBlur(image, blurFactor, 0, 0);
			else
				blurred = blurImage(image, (int)(smallerSide * blurFactor));
			
			time = System.currentTimeMillis();
			long blurTime = time - miliStart;
			miliStart = time;
			//Util.saveImage(blurred, "blur.png");
			
			generateDelta(image, blurred, delta, (int)(threshold * 255));
			time = System.currentTimeMillis();
			long deltaTime = time - miliStart;
			miliStart = time;
			
			dilate(delta, delta2, width, height);
			dilate(delta2, delta, width, height);
			dilate(delta, delta2, width, height);
			dilate(delta2, delta, width, height);
			
			time = System.currentTimeMillis();
			long dilateTime = time - miliStart;
			miliStart = time;
			
			BufferedImage filtered = Util.sameCanvasSize(image);
			//replaceMarked(image, filtered, blurred, delta);
			interpolateMarked(image, filtered, delta);
			time = System.currentTimeMillis();
			long interpolateTime = time - miliStart;
			miliStart = time;
			
			//Util.saveImage(filtered, "deforeground.png");
			
			if(reduceIntermediateResolution)
				blurred = smallBlur(filtered, blurFactor, 0, 0);
			else
				blurred = blurImage(filtered, (int)(smallerSide * blurFactor));
			//Util.saveImage(blurred, "deforeground-blur.png");
			time = System.currentTimeMillis();
			long blurFilteredTime = time - miliStart;
			miliStart = time;
			
			generateFinal(image, blurred, filtered);
			time = System.currentTimeMillis();
			long finalTime = time - miliStart;
			miliStart = time;
			
			if(printStats) 
			{
				System.out.println("1) Blur time        = " + blurTime + " ms");
				System.out.println("2) Delta time       = " + deltaTime + " ms");
				System.out.println("3) Dilate time      = " + dilateTime + " ms");
				System.out.println("4) Interpolate time = " + interpolateTime + " ms");
				System.out.println("5) Blur filt. time  = " + blurFilteredTime + " ms");
				System.out.println("6) Final time       = " + finalTime + " ms");
				
				System.out.println();
				System.out.println("Total time          = " + (System.currentTimeMillis() - totalStart) + " ms");
			}
			
			return filtered;
		}
		else
		{
			
			BufferedImage filtered = Util.sameCanvasSize(image);
			BufferedImage blurred = null;
			if(reduceIntermediateResolution)
				blurred = smallBlur(image, blurFactor, 0, 0);
			else
				blurred = blurImage(image, (int)(smallerSide * blurFactor));
			
			time = System.currentTimeMillis();
			long blurTime = time - miliStart;
			miliStart = time;
			
			generateFinal(image, blurred, filtered);
			
			time = System.currentTimeMillis();
			long finalTime = time - miliStart;
			
			miliStart = time;
			
			if(printStats) 
			{
				System.out.println("1) Blur time        = " + blurTime + " ms");
				System.out.println("2) Final time       = " + finalTime + " ms");
				
				System.out.println();
				System.out.println("Total time          = " + (System.currentTimeMillis() - totalStart) + " ms");
			}
			
			return filtered;
		}
	}
	
	private static class LinFunc
	{
		private float intY;
		private float slope;
		
		public LinFunc(float y, float slope)
		{
			intY = y;
			this.slope = slope;
		}
		
		public LinFunc(float x1, float y1, float x2, float y2)
		{
			slope = (y2 - y1) / (x2 - x1);
			intY = y1 - x1 * slope;
		}
		
		public float evaluateY(float x)
		{
			return intY + x * slope;
		}
		
		public float evaluateX(float y)
		{
			return (y - intY) / slope;
		}
		
		public float intersectX(LinFunc f)
		{
			return (f.intY - intY) / (slope - f.slope);
		}
		
		public float intersectY(LinFunc f)
		{
			return ((slope * f.intY) - (f.slope * intY)) / (slope - f.slope);
		}
	}
	
	private static LinFunc findBestFit(float[] X, float[] Y, int count)
	{
		float sumX = 0, sumY = 0, sumX2 = 0, sumXY = 0;
		
		for(int i = 0; i < count; ++i)
		{
			sumX += X[i];
			sumY += Y[i];
			sumX2 += X[i] * X[i];
			sumXY += X[i] * Y[i];
		}
		
		float xMean = sumX / count;
		float yMean = sumY / count;
		float slope = (sumXY - sumX * yMean) / (sumX2 - sumX * xMean);
		float yInt = yMean - slope * xMean;
		
		System.out.println("Fit equation: " + slope + " * x + " + yInt);
		
		return new LinFunc(yInt, slope);
	}
	
	
	private static int prefilterPoints(float[] x, float[] y, int count, float max, boolean vertical, Graphics2D g)
	{
		if(count < 2) return count;
		
		float[] dY = new float[count - 1];
		float sumDY = 0.0f;
		
		//max *= 0.2f;
		
		if(count % 2 == 1) count--;
		
		float cutoff = (max * 1.3f) * (max * 1.3f);
		//System.out.println("Cutoff : " + Math.sqrt(cutoff));
		//System.out.println("Before cutoff : " + count);
		
		for(int i = 0; i < count - 1; i += 2)
		{
			float dx = x[i] - x[i+1];
			float dy = y[i] - y[i+1];
			float delta =  dx * dx + dy * dy;
			
			//System.out.println("Delta = " + Math.sqrt(delta));
					
			if(delta > cutoff)
			{
				for(int n = i; n < count - 2; ++n)
				{
					x[n] = x[n + 2];
					y[n] = y[n + 2];
				}
				count -= 2;
				i -= 2;
			}
		}
		
		//System.out.println();
		//System.out.println("After cutoff = " + count);
		//System.out.println();
		
		/*
		for(int i = 0; i < count - 1; i += 2)
		{
			float dx = x[i] - x[i+1];
			float dy = y[i] - y[i+1];
			float delta =  dx * dx + dy * dy;
			
			System.out.println("Delta = " + Math.sqrt(delta));
		}
		
		*/
		
		for(int i = 0; i < count - 1; ++i)
		{
			float d = vertical ? (x[i + 1] - x[i]) : (y[i + 1] - y[i]);
			dY[i] = d;
			sumDY += d;
		}
		
		Arrays.sort(dY);
		
		float avg = sumDY / count;
		float median = dY[count / 2];
		
		//System.out.println("[[[deltas]]]");
		//for(int i = 0; i < count - 1; ++i)
		//	System.out.println(dY[i]);
		//System.out.println();
		
		/*
		if(vertical)
		{
			System.out.println("X-axis filter - avg Y = " + avg);
			System.out.println("X-axis filter - med Y = " + median);
		}
		else
		{
			System.out.println("Y-axis filter - avg X = " + avg);
			System.out.println("Y-axis filter - med X = " + median);
		}
		*/
		
		float slope = (avg + median) * 0.5f;
		
		float minCost = count * max;
		minCost *= minCost;
		int minPoint = -1;
		
		for(int i = 0; i < count; ++i)
		{
			float cost = 0.0f;
			for(int X = 0; X < count; ++X)
			{
				float delta = 
						vertical ?
						Math.abs(x[i] + (slope * (X - i)) - x[X]) :
						Math.abs(y[i] + (slope * (X - i)) - y[X]);
				cost += delta * delta;
			}
			
			if(cost < minCost)
			{
				minCost = cost;
				minPoint = i;
			}
		}
		
		//System.out.println("Min point = " + minPoint);
		//System.out.println("Min cost  = " + minCost);
		//System.out.println("Start count = " + count);
		 
		if(minPoint >= 0)
		{
			/*
			if(vertical)
			{
				int p = minPoint;
				g.setColor(Color.pink);
				g.drawLine((int)(x[p] - 100 * slope), (int)(y[p] - 100), (int)(x[p] + 100 * slope), (int)(y[p] + 100));
			}
			else
			{
				int p = minPoint;
				g.setColor(Color.pink);
				g.drawLine((int)(x[p] - 100), (int)(y[p] - 100 * slope), (int)(x[p] + 100), (int)(y[p] + 100 * slope));
			}
			*/
			
			for(int i = 0; i < count; ++i)
			{
				float delta = 
						vertical ?
						Math.abs(x[minPoint] + (slope * (i - minPoint)) - x[i]) :
						Math.abs(y[minPoint] + (slope * (i - minPoint)) - y[i]);
				
						if(delta > max)
				{
					for(int n = i; n < count - 1; ++n)
					{
						x[i] = x[i + 1];
						y[i] = y[i + 1];
					}
					count--;
				}
			}
		}
		
		//System.out.println("End count = " + count);
		//System.out.println();
		
		return count;
	}
	
	private static void drawPoint(Graphics2D g, float[] x, float[] y, int count, Color c)
	{
		g.setColor(c);
		for(int i = 0; i < count; ++i)
		{
			g.fillRect((int)x[i] - 1, (int)y[i] - 1, 3, 3);
		}
	}
	
	private static int[] getAutocropCoords(BufferedImage image, float marginPerc, int marginFix, boolean clamp)
	{
		byte[] pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
		int width = image.getWidth();
		int height = image.getHeight();
		
		final int MAX_SUM = 10;
		
		int[] histoX = new int[height];
		int[] histoY = new int[width];
		int[] sumY = new int[height];
		
		int pixCount = width * height;
		int sumX = 0;
		
		int maxX = 0;
		int maxY = 0;
		
		for(int i = 0; i < pixCount; ++i)
		{
			int x = i % width;
			int y = i / width;
			
			if(i % width == 0) sumX = 0;
			
			if(pixels[i] < 0)
			{
				if(sumX < MAX_SUM) sumX++;
				if(sumY[x] < MAX_SUM) sumY[x]++;
				
				histoX[y] += sumX;
				histoY[x] += sumY[x];
				
				if(histoX[y] > maxY) maxY = histoX[y];
				if(histoY[x] > maxX) maxX = histoY[x];
			}
			else
			{
				sumX = 0;
				sumY[x] = 0;
			}
		}
		
		int threshX = (int)(maxX * 0.1f);
		int threshY = (int)(maxY * 0.1f);
		
		int x0 = 0, y0 = 0, x1 = 0, y1 = 0;
		
		for(int x = 0; x < width; ++x)
		{
			if(histoY[x] >= threshX)
			{
				x0 = x;
				break;
			}
		}
		
		for(int x = width - 1; x >= 0; --x)
		{
			if(histoY[x] >= threshX)
			{
				x1 = x;
				break;
			}
		}
		
		for(int y = 0; y < height; ++y)
		{
			if(histoX[y] >= threshY)
			{
				y0 = y;
				break;
			}
		}
		
		for(int y = height - 1; y >= 0; --y)
		{
			if(histoX[y] >= threshY)
			{
				y1 = y;
				break;
			}
		}
		
		//for(int i = 0; i < height; ++i)
		//{
		//	System.out.println("Histo line " + i + " = " + histoX[i]);
		//}
		
		//final float marginPerc = 0.05f;
		//final int marginFix = 6;
		
		
		int margin = marginFix + (int)(((width < height) ? width : height) * marginPerc);
		
		x0 -= margin;
		y0 -= margin;
		x1 += margin;
		y1 += margin;
		
		if(clamp)
		{
			if(x0 < 0) x0 = 0;
			if(y0 < 0) y0 = 0;
			if(x1 >= width) x1 = width - 1;
			if(y1 >= height) y1 = height - 1;
		}
		
		System.out.println("Crop X: " + x0 + " to " + x1 + " (orig w = " + width + ")");
		System.out.println("Crop Y: " + y0 + " to " + y1 + " (orig h = " + height + ")");
		
		return new int[] { x0, y0, x1, y1 };
	}
	
	public static BufferedImage autoCropBW(BufferedImage image)
	{
		int x0, y0, x1, y1;
		
		int[] crop = getAutocropCoords(image, 0.0f, 0, true);
		x0 = crop[0];
		y0 = crop[1];
		x1 = crop[2];
		y1 = crop[3];
		
		System.out.println("Crop X: " + x0 + " to " + x1);
		System.out.println("Crop Y: " + y0 + " to " + y1);
		
		int targetW = x1 - x0 + 1;
		int targetH = y1 - y0 + 1;
		
		BufferedImage result = new BufferedImage(targetW, targetH, BufferedImage.TYPE_BYTE_GRAY);
		result.getGraphics().drawImage(image, 0, 0, targetW, targetH, x0, y0, x1, y1, null);
		
		return image;
	}
	
	public static float[] findPaper(BufferedImage image)
	{
		float sfactor = 1.0f;
		
		if(reduceIntermediateResolution)
		{
			int width = image.getWidth();
			int height = image.getHeight();
			int smaller = (width > height) ? height : width;
			if(smaller > 256)
			{
				sfactor = 256.0f / (float)smaller;
				width = (int)(width * sfactor);
				height = (int)(height * sfactor);
				
				BufferedImage tmp = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
				tmp.getGraphics().drawImage(image, 0, 0, width, height, null);
				image = tmp;
			}
		}
		
		int width = image.getWidth();
		int height = image.getHeight();
		
		final int SAMPLE_COUNT = 40;
		final int RUNNING_SUM = 10;
		
		final float SENSITIVITY = 1.2f;
		
		BufferedImage preview = Util.sameCanvasSize(image);
		Graphics2D g = (Graphics2D)preview.getGraphics();
		g.drawImage(image, 0, 0, null);
		
		BufferedImage thresholded = autoThreshold(image);
		
		//autoCropBW(thresholded);
		int[] crops = getAutocropCoords(thresholded, 0.05f, 6, true);
		int cropX0 = crops[0];
		int cropY0 = crops[1];
		int cropX1 = crops[2];
		int cropY1 = crops[3];
		int cropW = cropX1 - cropX0 + 1;
		int cropH = cropY1 - cropY0 + 1;
		
		//g.drawImage(thresholded, 0, 0, null);
		
		g.setColor(Color.cyan);
		g.drawRect(cropX0, cropY0, cropW, cropH);
		
		byte[] pixels = ((DataBufferByte) thresholded.getRaster().getDataBuffer()).getData();
		
		float[] horX1 = new float[SAMPLE_COUNT];
		float[] horY1 = new float[SAMPLE_COUNT];
		float[] horX2 = new float[SAMPLE_COUNT];
		float[] horY2 = new float[SAMPLE_COUNT];
		int sxCount = 0;
		int exCount = 0;
		
		float[] verX1 = new float[SAMPLE_COUNT];
		float[] verY1 = new float[SAMPLE_COUNT];
		float[] verX2 = new float[SAMPLE_COUNT];
		float[] verY2 = new float[SAMPLE_COUNT];
		int syCount = 0;
		int eyCount = 0;
		
		System.out.println("Starting W = " + width);
		System.out.println("Starting H = " + height);
		
		for(int i = 0; i < SAMPLE_COUNT; ++i)
		{
			int y = cropY0 + (int)((float)i / (float)SAMPLE_COUNT * cropH + ((float)cropH / (float)SAMPLE_COUNT * 0.5f));
			int runningSum1 = 0;
			int runningSum2 = 0;
			
			//System.out.println("Y = " + y);
			
			for(int x = cropX0; x < cropX0 + RUNNING_SUM; ++x)
			{
				int p = (y * width + x); // * 3;
				runningSum1 += (pixels[p+0] & 0xFF); // + (pixels[p+1] & 0xFF) + (pixels[p+2] & 0xFF); 
			}
			
			for(int x = cropX0 + RUNNING_SUM + 1; x < cropX0 + RUNNING_SUM * 2 + 1; ++x)
			{
				int p = (y * width + x); // * 3;
				runningSum2 += (pixels[p+0] & 0xFF); // + (pixels[p+1] & 0xFF) + (pixels[p+2] & 0xFF); 
			}
			
			//System.out.println("STARTING [" + runningSum1 + "]  [" + runningSum2 + "]");
			
			float maxFac = 0.0f;
			float minFac = 100.0f;
			int minX = 0;			
			int maxX = 0;
			
			for(int x = cropX0 + RUNNING_SUM; x < cropX1 - RUNNING_SUM - 2; ++x)
			{
				int pL = (y * width + (x - RUNNING_SUM)); // * 3;
				int pM = (y * width + (x)); // * 3;
				int pN = (y * width + (x + 1)); // * 3;
				int pR = (y * width + (x + RUNNING_SUM + 1)); // * 3;
				int valL = (pixels[pL+0] & 0xFF); // + (pixels[pL+1] & 0xFF) + (pixels[pL+2] & 0xFF);
				int valM = (pixels[pM+0] & 0xFF); // + (pixels[pM+1] & 0xFF) + (pixels[pM+2] & 0xFF);
				int valN = (pixels[pN+0] & 0xFF); // + (pixels[pN+1] & 0xFF) + (pixels[pN+2] & 0xFF);
				int valR = (pixels[pR+0] & 0xFF); // + (pixels[pR+1] & 0xFF) + (pixels[pR+2] & 0xFF);
				
				float factor = (float)runningSum2 / (float)runningSum1;
				if(runningSum1 == 0 || runningSum2 == 0) factor = 1.0f;

				if(factor > maxFac) { maxFac = factor; maxX = x; }
				if(factor < minFac) { minFac = factor; minX = x; }
				
				runningSum1 -= valL;
				runningSum1 += valM;
				
				runningSum2 -= valN;
				runningSum2 += valR;
			}
			
			//System.out.println("MinX = " + minX + "; minFactor = " + minFac);
			//System.out.println("MaxX = " + maxX + "; maxFactor = " + maxFac);
			
			if(maxX < cropX0 + (int)(cropW * 0.25f) && maxFac > SENSITIVITY)
			{
				//g.setColor(Color.green);
				//g.fillOval(maxX-2, y-2, 4, 4);
				//System.out.println("Drawing GREEN!");
				
				//startX[sxCount++] = maxX;
				horX1[sxCount] = maxX;
				horY1[sxCount] = y;
				sxCount++;
			}
			
			if(minX > cropX0 + (int)(cropW * 0.75f) && minFac < (1.0f / SENSITIVITY))
			{
				//g.setColor(Color.red);
				//g.fillOval(minX-2, y-2, 4, 4);
				//System.out.println("Drawing RED!");
				
				//endX[exCount++] = minX;
				horX2[exCount] = minX;
				horY2[exCount] = y;
				exCount++;
			}
			
			//break;
		}
		
		for(int i = 0; i < SAMPLE_COUNT; ++i)
		{
			int x = cropX0 + (int)((float)i / (float)SAMPLE_COUNT * cropW + ((float)cropW / (float)SAMPLE_COUNT * 0.5f));
			int runningSum1 = 0;
			int runningSum2 = 0;
			
			//System.out.println("X = " + x);
			
			for(int y = cropY0; y < cropY0 + RUNNING_SUM; ++y)
			{
				int p = (y * width + x); // * 3;
				runningSum1 += (pixels[p+0] & 0xFF); // + (pixels[p+1] & 0xFF) + (pixels[p+2] & 0xFF); 
			}
			
			for(int y = cropY0 + RUNNING_SUM + 1; y < cropY0 + RUNNING_SUM * 2 + 1; ++y)
			{
				int p = (y * width + x); // * 3;
				runningSum2 += (pixels[p+0] & 0xFF); // + (pixels[p+1] & 0xFF) + (pixels[p+2] & 0xFF); 
			}
			
			//System.out.println("STARTING [" + runningSum1 + "]  [" + runningSum2 + "]");
			
			float maxFac = 0.0f;
			float minFac = 100.0f;
			int minY = 0;
			int maxY = 0;
			
			for(int y = cropY0 + RUNNING_SUM; y < cropY1 - RUNNING_SUM - 2; ++y)
			{
				int pL = ((y - RUNNING_SUM) * width + x); // * 3;
				int pM = ((y) * width + x); // * 3;
				int pN = ((y + 1) * width + x); // * 3;
				int pR = ((y + RUNNING_SUM + 1) * width + x); // * 3;
				int valL = (pixels[pL+0] & 0xFF); // + (pixels[pL+1] & 0xFF) + (pixels[pL+2] & 0xFF);
				int valM = (pixels[pM+0] & 0xFF); // + (pixels[pM+1] & 0xFF) + (pixels[pM+2] & 0xFF);
				int valN = (pixels[pN+0] & 0xFF); // + (pixels[pN+1] & 0xFF) + (pixels[pN+2] & 0xFF);
				int valR = (pixels[pR+0] & 0xFF); // + (pixels[pR+1] & 0xFF) + (pixels[pR+2] & 0xFF);
				
				float factor = (float)runningSum2 / (float)runningSum1;
				if(runningSum1 == 0 || runningSum2 == 0) factor = 1.0f;

				if(factor > maxFac) { maxFac = factor; maxY = y; }
				if(factor < minFac) { minFac = factor; minY = y; }
				
				runningSum1 -= valL;
				runningSum1 += valM;
				
				runningSum2 -= valN;
				runningSum2 += valR;
			}
			
			//System.out.println("MinX = " + minX + "; minFactor = " + minFac);
			//System.out.println("MaxX = " + maxX + "; maxFactor = " + maxFac);
			
			if(maxY < cropY0 + (int)(cropH * 0.25f) && maxFac > SENSITIVITY)
			{
				//g.setColor(Color.blue);
				//g.fillOval(x-2, maxY-2, 4, 4);
				//System.out.println("Drawing BLUE!");
				
				//startY[syCount++] = maxY;
				verX1[syCount] = x;
				verY1[syCount] = maxY;
				syCount++;
			}
			
			if(minY > cropY0 + (int)(cropH * 0.75f) && minFac < (1.0f / SENSITIVITY))
			{
				//g.setColor(Color.yellow);
				//g.fillOval(x-2, minY-2, 4, 4);
				//System.out.println("Drawing YELLOW!");
				
				//endY[eyCount++] = minY;
				verX2[eyCount] = x;
				verY2[eyCount] = minY;
				eyCount++;
			}
			
			//break;
		}
		
		sxCount = prefilterPoints(horX1, horY1, sxCount, (float)height / (float)SAMPLE_COUNT, false, g);
		exCount = prefilterPoints(horX2, horY2, exCount, (float)height / (float)SAMPLE_COUNT, false, g);
		
		syCount = prefilterPoints(verX1, verY1, syCount, (float)width / (float)SAMPLE_COUNT, true, g);
		eyCount = prefilterPoints(verX2, verY2, eyCount, (float)width / (float)SAMPLE_COUNT, true, g);
		
		drawPoint(g, horX1, horY1, sxCount, Color.green);
		drawPoint(g, horX2, horY2, exCount, Color.red);
		
		drawPoint(g, verX1, verY1, syCount, Color.blue);
		drawPoint(g, verX2, verY2, eyCount, Color.yellow);
		
		//syCount = prefilterPointsY(verX1, verY1, syCount, (float)width / (float)SAMPLE_COUNT);
		//eyCount = prefilterPointsY(verX1, verY1, exCount, (float)width / (float)SAMPLE_COUNT);
		
		LinFunc edgeX1 = findBestFit(horX1, horY1, sxCount);
		LinFunc edgeX2 = findBestFit(horX2, horY2, exCount);
		
		LinFunc edgeY1 = findBestFit(verX1, verY1, syCount);
		LinFunc edgeY2 = findBestFit(verX2, verY2, eyCount);
		
		{
			float x1 = 0.0f;
			float y1 = edgeX1.evaluateY(x1);
			float x2 = width;
			float y2 = edgeX1.evaluateY(x2);
			g.setColor(Color.green);
			g.drawLine((int)x1, (int)y1, (int)x2, (int)y2);
		}
		
		{
			float x1 = 0.0f;
			float y1 = edgeX2.evaluateY(x1);
			float x2 = width;
			float y2 = edgeX2.evaluateY(x2);
			g.setColor(Color.red);
			g.drawLine((int)x1, (int)y1, (int)x2, (int)y2);
		}
		
		{
			float y1 = 0.0f;
			float x1 = edgeY1.evaluateX(y1);
			float y2 = height;
			float x2 = edgeY1.evaluateX(y2);
			g.setColor(Color.blue);
			g.drawLine((int)x1, (int)y1, (int)x2, (int)y2);
		}
		
		{
			float y1 = 0.0f;
			float x1 = edgeY2.evaluateX(y1);
			float y2 = height;
			float x2 = edgeY2.evaluateX(y2);
			g.setColor(Color.yellow);
			g.drawLine((int)x1, (int)y1, (int)x2, (int)y2);
		}
		
		float edge1X = edgeX1.intersectX(edgeY1);
		float edge1Y = edgeX1.intersectY(edgeY1);
		
		float edge2X = edgeX1.intersectX(edgeY2);
		float edge2Y = edgeX1.intersectY(edgeY2);
		
		float edge3X = edgeX2.intersectX(edgeY1);
		float edge3Y = edgeX2.intersectY(edgeY1);
		
		float edge4X = edgeX2.intersectX(edgeY2);
		float edge4Y = edgeX2.intersectY(edgeY2);
		
		float[] edgesX = { edge1X, edge2X, edge3X, edge4X };
		float[] edgesY = { edge1Y, edge2Y, edge3Y, edge4Y };
		drawPoint(g, edgesX, edgesY, 4, Color.pink);
		
		float[] result = { edge1X / sfactor, edge1Y / sfactor,
				edge3X / sfactor, edge3Y / sfactor,
				edge2X / sfactor, edge2Y / sfactor,
				edge4X / sfactor, edge4Y / sfactor };
		
		Util.saveImage(preview, "preview.png");
		//ImageViewer.showImageWindow(preview, "Preview");
		
		System.out.println("DONE");
		return result;
	}
	
	private static float areaOfTriangle(float Ax, float Ay, float Bx, float By, float Cx, float Cy)
	{
		return Math.abs((Ax * (By - Cy) + Bx * (Cy - Ay) + Cx * (Ay - By)) / 2.0f);
	}
	
	private static float distance(float x1, float y1, float x2, float y2)
	{
		float dx = x2 - x1;
		float dy = y2 - y1;
		return (float) Math.sqrt(dx * dx + dy * dy);
	}
	
	public static BufferedImage invertPerspective(BufferedImage image, float[] points)
	{
		float[] pX = { points[0], points[2], points[4], points[6] };
		float[] pY = { points[1], points[3], points[5], points[7] };
		
		float area = areaOfTriangle(pX[0], pY[0], pX[1], pY[1], pX[2], pY[2]) +
				areaOfTriangle(pX[1], pY[1], pX[3], pY[3], pX[2], pX[2]);
		
		System.out.println("Effective area : " + (int)area);
		
		float sideTop = distance(pX[0], pY[0], pX[1], pY[1]);
		float sideBottom = distance(pX[2], pY[2], pX[3], pY[3]);
		float sideLeft = distance(pX[0], pY[0], pX[2], pY[2]);
		float sideRight = distance(pX[1], pY[1], pX[3], pY[3]);
		
		float[] aspects = {
				sideTop / sideLeft,
				sideTop / sideRight,
				sideBottom / sideLeft,
				sideBottom / sideRight
		};
		
		float aspectRatio = (aspects[0] + aspects[1] + aspects[2] + aspects[3]) / 4.0f;
		
		System.out.println("Aspect ratio = " + aspectRatio);
		
		int tHeight = (int)Math.sqrt(area / aspectRatio);
		int tWidth = (int)(area / (float)tHeight);
		
		System.out.println("Source size = " + image.getWidth() + " x " + image.getHeight());
		System.out.println("Target size = " + tWidth + " x " + tHeight);
		
		if(tWidth == 0 || tHeight == 0)
		{
			System.out.println("Invalid transform, probably couldn't find paper");
			return null;
		}
		
		BufferedImage result = new BufferedImage(tWidth, tHeight, BufferedImage.TYPE_3BYTE_BGR);
		byte[] pixelsS = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
		byte[] pixelsD = ((DataBufferByte) result.getRaster().getDataBuffer()).getData();
		
		float topVecX = pX[1] - pX[0];
		float topVecY = pY[1] - pY[0];
		
		float bottomVecX = pX[3] - pX[2];
		float bottomVecY = pY[3] - pY[2];
		
		float leftVecX = pX[2] - pX[0];
		float leftVecY = pY[2] - pY[0];
		
		float rightVecX = pX[3] - pX[1];
		float rightVecY = pY[3] - pY[1];
		
		LinFunc diag1 = new LinFunc(pX[0], pY[0], pX[3], pY[3]);
		LinFunc diag2 = new LinFunc(pX[2], pY[2], pX[1], pY[1]);
		
		float diagX = diag1.intersectX(diag2);
		float diagY = diag1.intersectY(diag2);
		
		System.out.println("topVec : " + topVecX + ", " + topVecY);
		System.out.println("bottomVec : " + bottomVecX + ", " + bottomVecY);
		System.out.println("leftVec : " + leftVecX + ", " + leftVecY);
		System.out.println("rightVec : " + rightVecX + ", " + rightVecY);
		
		System.out.println();
		System.out.println("Diag center : " + (int)diagX + ", " + (int)diagY);
		
		float d0 = distance(pX[0], pY[0], diagX, diagY);
		float d1 = distance(pX[1], pY[1], diagX, diagY);
		float d2 = distance(pX[3], pY[3], diagX, diagY);
		float d3 = distance(pX[2], pY[2], diagX, diagY);
		
		System.out.println("d0 = " + d0);
		System.out.println("d1 = " + d1);
		System.out.println("d2 = " + d2);
		System.out.println("d3 = " + d3);
		
		float q0 = (d0 + d2) / d2;
		float q1 = (d1 + d3) / d3;
		float q2 = (d0 + d2) / d0;
		float q3 = (d1 + d3) / d1;
		
		System.out.println();
		System.out.println("q0 = " + q0);
		System.out.println("q1 = " + q1);
		System.out.println("q2 = " + q2);
		System.out.println("q3 = " + q3);
		
		q0 *= 0.5f;
		q1 *= 0.5f;
		q2 *= 0.5f;
		q3 *= 0.5f;
		
		int sWidth = image.getWidth();
		int sHeight = image.getHeight();
		
		for(int y = 0; y < tHeight; ++y)
		{
			int oY = y * tWidth * 3;
			float cY = (float)y / (float)tHeight;
			
			for(int x = 0; x < tWidth; ++x)
			{
				int p = oY + x * 3;
				
				float cX = (float)x / (float)tWidth;
				
				float df0 = (float)Math.sqrt(cX * cX + cY * cY);
				float df1 = (float)Math.sqrt((1.0f - cX) * (1.0f - cX) + cY * cY);
				float df2 = (float)Math.sqrt(cX * cX + (1.0f - cY) * (1.0f - cY));
				float df3 = (float)Math.sqrt((1.0f - cX) * (1.0f - cX) + (1.0f - cY) * (1.0f - cY));
				
				//if(x == 100 && y == 100)
				//{
				//	System.out.println("df0 = " + df0);
				//	System.out.println("df1 = " + df1);
				//	System.out.println("df2 = " + df2);
				//	System.out.println("df3 = " + df3);
				//}
				
				df0 = cX + cY;
				df1 = (1.0f - cX) + cY;
				df2 = cX + (1.0f - cY);
				df3 = (1.0f - cX) + (1.0f - cY);
				
				if(df0 < 0.00001f) df0 = 0.00001f;
				if(df1 < 0.00001f) df1 = 0.00001f;
				if(df2 < 0.00001f) df2 = 0.00001f;
				if(df3 < 0.00001f) df3 = 0.00001f;
				
				df0 = 1.0f / df0;
				df1 = 1.0f / df1;
				df2 = 1.0f / df2;
				df3 = 1.0f / df3;
				
				//float dsum = df0 + df1 + df2 + df3;
				
				//df0 = dsum - df0;
				//df1 = dsum - df1;
				//df2 = dsum - df2;
				//df3 = dsum - df3;
				
				float dsum = df0 + df1 + df2 + df3;
				df0 /= dsum;
				df1 /= dsum;
				df2 /= dsum;
				df3 /= dsum;
				
				/*
				df0 /= q0;
				df1 /= q1;
				df2 /= q3;
				df3 /= q2;
				*/
				
				//df0 *= q0;
				//df1 *= q1;
				//df2 *= q3;
				//df3 *= q2;
				
				/*
				System.out.println();
				System.out.println("df0 = " + df0);
				System.out.println("df1 = " + df1);
				System.out.println("df2 = " + df2);
				System.out.println("df3 = " + df3);
				System.out.println();
				*/
				
				float svX0 = pX[0] + cX * topVecX + cY * leftVecX;
				float svY0 = pY[0] + cX * topVecY + cY * leftVecY;
				
				float svX1 = pX[1] - (1.0f - cX) * topVecX + cY * rightVecX;
				float svY1 = pY[1] - (1.0f - cX) * topVecY + cY * rightVecY;
				
				float svX2 = pX[2] + cX * bottomVecX - (1.0f - cY) * leftVecX;
				float svY2 = pY[2] + cX * bottomVecY - (1.0f - cY) * leftVecY;
				
				float svX3 = pX[3] - (1.0f - cX) * bottomVecX - (1.0f - cY) * rightVecX;
				float svY3 = pY[3] - (1.0f - cX) * bottomVecY - (1.0f - cY) * rightVecY;
				
				/*
				System.out.println("svX0 = " + svX0);
				System.out.println("svY0 = " + svY0);
				System.out.println("svX1 = " + svX1);
				System.out.println("svY1 = " + svY1);
				System.out.println("svX2 = " + svX2);
				System.out.println("svY2 = " + svY2);
				System.out.println("svX3 = " + svX3);
				System.out.println("svY3 = " + svY3);
				System.out.println();
				System.out.println("fsum = " + (df0 + df1 + df2 + df3));
				System.out.println();
				*/
				
				
				float fX = df0 * svX0 + df1 * svX1 + df2 * svX2 + df3 * svX3;
				float fY = df0 * svY0 + df1 * svY1 + df2 * svY2 + df3 * svY3;
				
				int sX = (int)fX;
				int sY = (int)fY;
				float fracX = fX - sX;
				float fracY = fY - sY;
				
				//System.out.println("sX = " + sX);
				//System.out.println("sY = " + sY);
				
				//System.out.println();
				
				/*
				float vecXx = (1.0f - cY) * topVecX + (cY) * bottomVecX;
				float vecXy = (1.0f - cY) * topVecY + (cY) * bottomVecY;
				
				float vecYx = (1.0f - cX) * leftVecX + (cX) * rightVecX;
				float vecYy = (1.0f - cX) * leftVecY + (cX) * rightVecY;
				
				//(int)(pX[0] + cX * vecXx + cX * vecXy + cY * vecYx + cY * vecYy);
				int sX = (int)(pX[0] + cX * vecXx + cY * vecYx);
				int sY = (int)(pY[0] + cX * vecXy + cY * vecYy);
				*/
				
				if(sX < 0) sX = 0;
				if(sY < 0) sY = 0;
				if(sX >= sWidth) sX = sWidth - 1;
				if(sY >= sHeight) sY = sHeight - 1;
				
				//pixelsD[p+0] = pixelsS[(sY * sWidth + sX) * 3 + 0];
				//pixelsD[p+1] = pixelsS[(sY * sWidth + sX) * 3 + 1];
				//pixelsD[p+2] = pixelsS[(sY * sWidth + sX) * 3 + 2];
				bilSample(pixelsS, sX, sY, fracX, fracY, sWidth, sHeight, pixelsD, p);
			}
		}
		
		//Util.saveImage(result, "perspective.png");
		return result;
	}
	
	private static void bilSample(byte[] pixels, int x, int y, float fx, float fy, int width, int height, byte[] result, int roffset)
	{
		int pR00 = pixels[((y) * width + (x)) * 3 + 0] & 0xFF;
		int pG00 = pixels[((y) * width + (x)) * 3 + 1] & 0xFF;
		int pB00 = pixels[((y) * width + (x)) * 3 + 2] & 0xFF;
		
		int pR10 = pixels[((y) * width + (x+1)) * 3 + 0] & 0xFF;
		int pG10 = pixels[((y) * width + (x+1)) * 3 + 1] & 0xFF;
		int pB10 = pixels[((y) * width + (x+1)) * 3 + 2] & 0xFF;
		
		int pR01 = pixels[((y+1) * width + (x)) * 3 + 0] & 0xFF;
		int pG01 = pixels[((y+1) * width + (x)) * 3 + 1] & 0xFF;
		int pB01 = pixels[((y+1) * width + (x)) * 3 + 2] & 0xFF;
		
		int pR11 = pixels[((y+1) * width + (x+1)) * 3 + 0] & 0xFF;
		int pG11 = pixels[((y+1) * width + (x+1)) * 3 + 1] & 0xFF;
		int pB11 = pixels[((y+1) * width + (x+1)) * 3 + 2] & 0xFF;
		
		float topR = pR00 + fx * (pR10 - pR00);
		float topG = pG00 + fx * (pG10 - pG00);
		float topB = pB00 + fx * (pB10 - pB00);
		
		float botR = pR01 + fx * (pR11 - pR01);
		float botG = pG01 + fx * (pG11 - pG01);
		float botB = pB01 + fx * (pB11 - pB01);
		
		result[roffset + 0] = (byte)((int)(topR + fy * (botR - topR)));
		result[roffset + 1] = (byte)((int)(topG + fy * (botG - topG)));
		result[roffset + 2] = (byte)((int)(topB + fy * (botB - topB)));
	}
	
	public static BufferedImage autoDePaper(BufferedImage source)
	{
		long sTime = System.currentTimeMillis();
		float[] points = DePaper.findPaper(source);
		System.out.println("Find paper time : " + (System.currentTimeMillis() - sTime) + " ms");
		
		sTime = System.currentTimeMillis();
		BufferedImage projected = DePaper.invertPerspective(source, points);
		System.out.println("Invert perspective time : " + (System.currentTimeMillis() - sTime) + " ms");
		
		return DePaper.process(projected, 0.05f, 0.5f, true);
	}
	
	public static BufferedImage autoDePaper(String fileName)
	{
		BufferedImage source = Util.loadImage(fileName);
		if(source == null) return null;
		return autoDePaper(source);
	}
}
